/**
 * Your obedient servant.
 * 
 * @author back2dos
 */
class Minion {
	private var target:Object;
	private var hooks:/*Function*/Array;
	private var asap:Boolean;
	private var duration:Number = 0;
	private var values:Object;
	private var easing:Function;
	private var m:Minion;
	private var spells:/*Spell*/Array;
	private function Minion(target:Object, manual:Boolean) {
		this.target = target;
		this.hooks = [];
		this.asap = (manual != true);
	}
	/**
	 * Will summon a new Minion, that will start simultaneously with this one
	 * @param	target
	 * @return
	 */
	public function meanwhile(target:Object):Minion {
		return this.m = new Minion(target || this.target, true);
	}
	/**
	 * Instructs Minion to invoke a function upon task completion.
	 * Binds the Function as in Minion.bind if more than one argument is passed
	 * @param	c
	 * @param	thisArg
	 * @return
	 */
	public function whenDone(c:Function, thisArg:Object):Minion {
		if (arguments.length > 1) 
			c = bind.apply(null, arguments);
		hooks.push(c);
		return this;
	}
	/**
	 * Will instruct the Minion to wait
	 * @see chain
	 * @param	duration
	 * @return
	 */
	public function wait(duration:Number):Minion {
		var target:Object = { };
		target[Math.random()] = null;
		return brief(target, duration);
	}
	/**
	 * Briefs a minion to carry out a task. Should this minion already be assigned a task,
	 * it will forward it to a freshly summoned and chained one.
	 * @param	values
	 * @param	duration
	 * @param	easing
	 * @return
	 */
	private function brief(values:Object, duration:Number, easing:Function):Minion {
		if (this.values) 
			return chain().brief(values, duration, easing);
		else {
			this.values = values;
			this.duration = (duration < 0) ? -duration * TEMPO : duration || TEMPO;
			this.easing = easing || EASE;
			if (this.asap) 
				this.go();
			return this;
		}
	}
	/**
	 * Chains a new Minion to this one, that will carry out its work right after this one is done.
	 * A chained minion is automatically created for any but the first instruction given to a Minion.
	 * @see meanwhile
	 * 
	 * @param	target
	 * @return
	 */
	public function chain(target:Object):Minion {
		var ret:Minion = new Minion(target || this.target, true);
		whenDone(ret.go, ret);
		return ret;
	}
	/**
	 * Instructs the minion to modify the properties of its target object.
	 * @see chain
	 * @param	values
	 * @param	duration the duration in msecs
	 * @param	easing
	 * @return
	 */
	public function to(values:Object, duration:Number, easing:Function):Minion {
		return this.brief(values, duration, easing);
	}
	/**
	 * Cancels the spell cast to transform a given property.
	 * @param	property
	 * @param	finish @see Spell::cancel
	 * @param	callback @see Spell::cancel
	 */
	private function cancel(property:String, finish:Boolean, callback:Boolean):Void {
		this.spells[property].cancel(finish);
		delete this.spells[property];
	}
	/**
	 * 
	 * @param	property
	 * @param	from
	 * @param	to
	 * @param	progress
	 */
	private function translatio(property:String, from:Number, to:Number, progress:Number):Void {
		this.target[property] = from + (to - from) * easing(progress);
	}
	/**
	 * Casts a spell on a property.
	 * @param	magic
	 * @param	property
	 */
	private function cast(magic:Function, property:String) {
		if (target.minions[property]) 
			target.minions[property].cancel(property);
		target.minions[property] = this;
		this.spells[property] = new Spell(this.duration, magic, bind(this.finish, this));		
	}
	/**
	 * Finalizes a minions task.
	 */
	private function finish():Void {
		if (this.spells) {
			this.spells = null;
			for (var i:Number = 0; i < hooks.length; i++) 
				hooks[i]();			
		}
	}
	/**
	 * Orders the minion to stop its work.
	 * @param	finish if true, the Minion will instantly put its target object to the intended final state
	 * @return
	 */
	public function terminate(finish:Boolean):Minion {
		for (var property:String in spells) 
			this.cancel(property, finish);
		this.m.terminate(finish);
		return this;
	}
	/**
	 * Orders the minion to commence its work. If it's already doing it, it will start all over. 
	 * Such is the nature of these stupid beasts.
	 */
	public function go():Minion {
		for (var property:String in this.spells)
			this.spells[property].update(0);
		this.spells = [];
		if (!target.minions) 
			target.minions = { };
		for (var property:String in values) 
			cast(bind(translatio, this, property, target[property], values[property]), property);
		this.m.go();
		return this;
	}
	/**
	 * The default spell duration in miliseconds
	 */
	static public var TEMPO:Number = 300;
	/**
	 * The default spell duration in miliseconds
	 */
	static public var EASE:Function = Math.sqrt;
	/**
	 * Overlords don't use constructors. They summon.
	 * Honoring this fundametal difference costs about 25B in file size and is totally worth it.
	 * 
	 * @param	target
	 * @param	manual
	 * @return
	 */
	static public function summon(target:Object, manual:Boolean):Minion {
		return new Minion(target, manual);
	}
	/**
	 * Will cleanse an object of all minions.
	 * @param	target
	 * @param	finish
	 */
	static public function cleanse(target:Object, finish:Boolean):Void {
		for (var property:String in target.minions) 
			target.minions[property].terminate(finish);
	}
	/**
	 * Binds a function to an object. Any further arguments will be bound as parameters.
	 * @param	target
	 * @param	thisArg
	 * @return
	 */
	static public function bind(target:Function, thisArg:Object):Function {
		var a:Array = arguments.slice(2);
		return function () {
			return target.apply(thisArg, a.concat(arguments));
		}
	}
	/**
	 * Binds a function to an object. Any further arguments will be bound as parameters from the right.
	 * @param	target
	 * @param	thisArg
	 * @return
	 */
	static public function rBind(target:Function, thisArg:Object):Function {
		var a:Array = arguments.slice(2);
		return function () {
			return target.apply(thisArg, arguments.concat(a));
		}
	}
	/**
	 * Transforms a standard easing equation (as defined by Robert Penner) into a Minion ease function.
	 * The bound function is cached per easing equation, hence calling ease with the same parameter twice
	 * will return the same function.
	 * 
	 * @param	penner
	 * @return
	 */
	static public function ease(penner:Function):Function {
		return penner.__t || (penner.__t = rBind(penner, null, 0, 1, 1));
	}
}